﻿using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Data.Entity.Infrastructure;
using System.Diagnostics.Contracts;
using System.Linq;
using DynamicFiltering;
using System.Data.Entity.Core.Objects;

namespace AdventureWorksImplementation
{

    internal class TARGET_ENTITIY_NAMES
    {
        internal readonly string PRODUCT = typeof( Product ).Name;
        internal readonly string WORK_ORDER = typeof( WorkOrder ).Name;
        internal readonly string LOCATION = typeof( Location ).Name;
    }

    #region Custom Exceptions

    public class BuildChainException : Exception
    {
        private readonly string _message;
        public BuildChainException( string sourceEntityName, string targetEntityName )
        {
            _message = string.Format( "Unable to build referential chain between {0} and {1}", sourceEntityName, targetEntityName );
        }
        public override string Message
        {
            get
            {
                return _message;
            }
        }
    }

    public class DynamicDataFilterException : Exception
    {
        private readonly string _message;
        public DynamicDataFilterException( bool compilation )
        {
            var action = compilation ? "compile" : "execute";
            _message = string.Format( "DynamicDataFilter failed to {0} generated code.", action );
        }
        public override string Message
        {
            get
            {
                return _message;
            }
        }
    }

    #endregion

    public class DynamicFilterWrapper
    {

        private readonly int _productId;
        private readonly int? _locationId;
        private readonly List<int> _workOrderIds;
        private readonly ObjectContext _contextForEntityNavigation;
        private readonly AdventureWorksModelContainer _dbContext;
        private readonly List<EntityModelNavigator> _entityModelNavigatorList;
        private string _sourceEntityTypeName;
        private readonly TARGET_ENTITIY_NAMES _targetEntityNames = new TARGET_ENTITIY_NAMES();

        /// <summary>
        /// Note that we're accepting an open context; we aren't responsible for closing it
        /// </summary>
        public DynamicFilterWrapper( AdventureWorksModelContainer context, int productId, List<int> workOrderIds = null, int? locationId = null )
        {
            _productId = productId;
            _workOrderIds = workOrderIds;
            _locationId = locationId;
            _dbContext = context;
            _contextForEntityNavigation = ( (IObjectContextAdapter)_dbContext ).ObjectContext;
            _entityModelNavigatorList = new List<EntityModelNavigator>();
        }

        /// <summary>
        /// _context.DefaultContainerName
            //"AdventureWorksModelContainer"
        /// </summary>
        /// <typeparam name="AdventureWorksEntityType"></typeparam>
        /// <param name="passThroughEntityName">See Comments above EntityModelNavigator.BuildShortestChain</param>
        /// <returns></returns>
        public IQueryable<AdventureWorksEntityType> FilteredEntities<AdventureWorksEntityType>(string passThroughEntityName = "") where AdventureWorksEntityType : class, new()
        {
            _sourceEntityTypeName = typeof( AdventureWorksEntityType ).Name;
            BuildEntityNavigatorList( passThroughEntityName );
            return GenerateIQueryable<AdventureWorksEntityType>();
        }

        private void BuildEntityNavigatorList( string passThroughEntityName )
        {
            Contract.Invariant( _sourceEntityTypeName != null );
            _entityModelNavigatorList.Clear();
            AddEntityNavigator( _targetEntityNames.PRODUCT, _productId, passThroughEntityName );
            if ( _workOrderIds != null )
            {
                AddEntityNavigator( _targetEntityNames.WORK_ORDER, _workOrderIds, passThroughEntityName );
            }
            if ( _locationId.HasValue )
            {
                AddEntityNavigator( _targetEntityNames.LOCATION, _locationId.Value, passThroughEntityName );
            }
        }

        private void AddEntityNavigator( string targetEntityName, object targetCondition, string passThroughEntityName )
        {
            var emn = new EntityModelNavigator( _contextForEntityNavigation ) { ToEntityFilter = targetCondition };
            if ( !emn.BuildShortestChain( _sourceEntityTypeName, targetEntityName, passThroughEntityName ) )
            {
                throw new BuildChainException( _sourceEntityTypeName, targetEntityName );
            }
            _entityModelNavigatorList.Add( emn );
        }

        private IQueryable<AdventureWorksEntityType> GenerateIQueryable<AdventureWorksEntityType>()
        {
            var query = new LinqGenerator().Generate( _entityModelNavigatorList, _dbContext );
            var compilerResult = new CompilerWrapper().Compile<AdventureWorksModelContainer>( query, new List<string>() { this.GetType().Assembly.Location } );
            if ( compilerResult.Errors.HasErrors )
            {
                throw new DynamicDataFilterException( true );
            }
            else
            {
                return GetIQueryableFromCompiledAssembly<AdventureWorksEntityType>( compilerResult );
            }
        }

        private IQueryable<AdventureWorksEntityType> GetIQueryableFromCompiledAssembly<AdventureWorksEntityType>( CompilerResults compilerResult )
        {
            var dynamicTypeName = string.Format( "{0}.{1}", LinqGenerator.NAMESPACE_NAME, LinqGenerator.CLASS_NAME );
            var method = compilerResult.CompiledAssembly.GetType( dynamicTypeName ).GetMethod( LinqGenerator.METHOD_NAME );
            Contract.Requires( method != null );
            var methodResult = (IQueryable<AdventureWorksEntityType>)method.Invoke( null, GetMethodParameters() );
            if ( methodResult == null )
            {
                throw new DynamicDataFilterException( false );
            }
            else
            {
                return methodResult;
            }
        }

        /// <summary>
        /// Build the array of parameters that will go into the dynamic method.
        /// The data context is always passed in.
        /// Any int lists that may have been passed will also go, but regular ints don't need to because their value will have been
        /// hard-coded into the code before it was compiled.  See LinqGenerator.AddListCondition
        /// </summary>
        /// <returns></returns>
        private object[] GetMethodParameters()
        {
            var parametersThatAreLists = new List<List<int>>();
            _entityModelNavigatorList.ForEach( emn => { if ( emn.ToEntityFilter is List<int> ) parametersThatAreLists.Add( (List<int>)emn.ToEntityFilter ); } );
            var result = new object[parametersThatAreLists.Count + 1];
            result[0] = _dbContext;
            for ( int i = 0; i < parametersThatAreLists.Count; i++ )
            {
                result[i + 1] = parametersThatAreLists[i];
            }
            return result;
        }

    }
}
